package com.nc.mem.serial.graph;

import gnu.trove.strategy.IdentityHashingStrategy;

import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.objectweb.asm.Type;

import com.nc.mem.serial.graph.container.SerializerRegistry;

public final class Representation {
	private static final byte SP = -1;

	public static String INTERNAL_NAME;
	public static IdentityHashingStrategy<Object> IDENTITY_HASHING;

	private static final SerializerRegistry REPO;

	static {
		INTERNAL_NAME = Type.getInternalName(Representation.class);
		IDENTITY_HASHING = new IdentityHashingStrategy<Object>();
		REPO = SerializerRegistry.getInstance();
	}

	public static int classToId(final Class<?> val) {

		return REPO.classToId(val);
	}

	public static void copyUTF(final ByteBuffer src, final ByteBuffer dst) {
		writeString(dst, readString(src));
	}

	public static void dump(final PrintStream pw) {
		final Set<Entry<Integer, List<Class<?>>>> set = REPO.orderedView().entrySet();

		for (final Entry<Integer, List<Class<?>>> entry : set) {
			pw.println(entry.getKey() + " => " + transform(entry.getValue()));
		}

	}

	public static int idFor(final Object val) {
		return REPO.registerClass(val.getClass());
	}

	public static Class<?> idToClass(final ByteBuffer src) {

		return idToClass(readInt(src));
	}

	public static Class<?> idToClass(final int id) {
		return REPO.idToClass(id);
	}

	public static boolean readBoolean(final ByteBuffer src) {
		return src.get() == 1;
	}

	public static Boolean readBoolean(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return src.get() == 1;
		}
		return null;
	}

	public static byte readByte(final ByteBuffer src) {
		return src.get();
	}

	public static Byte readByte(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return src.get();
		}
		return null;
	}

	public static char readChar(final ByteBuffer src) {
		return src.getChar();
	}

	public static Character readCharacter(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return readChar(src);
		}
		return null;
	}

	public static double readDouble(final ByteBuffer src) {
		return src.getDouble();
	}

	public static Double readDouble(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return readDouble(src);
		}
		return null;
	}

	@SuppressWarnings("rawtypes")
	public static Enum readEnum(final ByteBuffer src, final Enum[] values, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return values[readShort(src)];
		}
		return null;
	}

	public static float readFloat(final ByteBuffer src) {
		return src.getFloat();
	}

	public static Float readFloat(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return readFloat(src);
		}
		return null;
	}

	public static int readInt(final ByteBuffer src) {
		for (int off = 0, result = 0; off < 32; off += 7) {
			final int b = src.get();
			result |= (b & 0x7F) << off;
			if ((b & 0x80) == 0) {
				return result;
			}
		}
		return 0;
	}

	public static Integer readInteger(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return readInt(src);
		}
		return null;
	}

	public static long readLong(final ByteBuffer src) {
		long result = 0;
		for (int off = 0; off < 64; off += 7) {
			final byte b = src.get();
			result |= (long) (b & 0x7F) << off;
			if ((b & 0x80) == 0) {
				return result;
			}
		}
		return 0;
	}

	public static Long readLong(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return readLong(src);
		}
		return null;
	}

	public static int readNullFlags(final ByteBuffer src) {
		return readInt(src);
	}

	public static Number readNumber(final ByteBuffer src) {
		switch (src.get()) {
		case 1:
			return src.get();
		case 2:
			return readShort(src);
		case 4:
			return readInt(src);
		case 8:
			return src.getFloat();
		case 16:
			return readLong(src);
		case 32:
			return src.getDouble();
		default:
			throw new UnsupportedOperationException();
		}
	}

	public static Number readNumber(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return readNumber(src);
		}
		return null;
	}

	public static short readShort(final ByteBuffer src) {
		final byte value = src.get();
		if (value == SP) {
			return src.getShort();
		}
		if (value < 0) {
			return (short) (value + 256);
		}

		return value;
	}

	public static Short readShort(final ByteBuffer src, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			return readShort(src);
		}
		return null;
	}

	public static String readString(final ByteBuffer src) {
		final int charCount = readInt(src);
		final char[] chars = new char[charCount];
		int c, charIndex = 0;
		while (charIndex < charCount) {
			c = src.get() & 0xff;
			switch (c >> 4) {
			case 0:
			case 1:
			case 2:
			case 3:
			case 4:
			case 5:
			case 6:
			case 7:
				chars[charIndex++] = (char) c;
				break;
			case 12:
			case 13:
				chars[charIndex++] = (char) ((c & 0x1F) << 6 | src.get() & 0x3F);
				break;
			case 14:
				chars[charIndex++] = (char) ((c & 0x0F) << 12 | (src.get() & 0x3F) << 6 | (src.get() & 0x3F) << 0);
				break;
			default:// checkstyle
				break;
			}

		}
		return new String(chars, 0, charCount);
	}

	private static List<String> transform(final List<Class<?>> value) {
		final ArrayList<String> list = new ArrayList<String>(value.size());

		for (final Class<?> c : value) {
			list.add(c.getName());
		}

		return list;
	}

	public static void writeBoolean(final ByteBuffer dest, final boolean val) {
		dest.put(val ? (byte) 1 : (byte) 0);
	}

	public static/* final */void writeBoolean(final ByteBuffer dest, final Boolean val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			dest.put(val ? (byte) 1 : 0);
		}
	}

	public static void writeByte(final ByteBuffer dest, final byte val) {
		dest.put(val);
	}

	public static/* final */void writeByte(final ByteBuffer dest, final Byte val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			dest.put(val);
		}
	}

	public static void writeChar(final ByteBuffer dest, final char val) {
		dest.putChar(val);
	}

	public static/* final */void writeCharacter(final ByteBuffer dest, final Character val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			writeChar(dest, val);
		}
	}

	public static void writeClassId(final ByteBuffer dest, final Class<?> clazz) {
		writeInt(dest, REPO.registerClass(clazz));
	}

	public static void writeClassId(final ByteBuffer dest, final Object val) {
		writeInt(dest, idFor(val));
	}

	public static void writeDouble(final ByteBuffer dest, final double val) {
		dest.putDouble(val);
	}

	public static/* final */void writeDouble(final ByteBuffer dest, final Double val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			writeDouble(dest, val);
		}
	}

	public static/* final */void writeEnum(final ByteBuffer dest, final Enum<?> enumeration, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			// dest.putShort((short) enumeration.ordinal());
			writeShort(dest, (short) enumeration.ordinal());
		}
	}

	public static void writeFloat(final ByteBuffer dest, final float val) {
		dest.putFloat(val);
	}

	public static/* final */void writeFloat(final ByteBuffer dest, final Float val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			writeFloat(dest, val);
		}
	}

	public static void writeInt(final ByteBuffer dest, final int value) {
		int val = value;
		if ((val & ~0x7F) == 0) {
			dest.put((byte) val);
			return;
		}
		for (int i = 0; i < 4; i++) {
			dest.put((byte) (val & 0x7F | 0x80));
			val >>>= 7;
			if ((val & ~0x7F) == 0) {
				dest.put((byte) val);
				return;
			}
		}
	}

	public static/* final */void writeInteger(final ByteBuffer dest, final Integer val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			writeInt(dest, val);
		}
	}

	public static void writeLong(final ByteBuffer buffer, final long value) {
		long val = value;
		if ((val & ~0x7FL) == 0) {
			buffer.put((byte) val);
			return;
		}
		for (int i = 0; i < 8; i++) {
			buffer.put((byte) ((int) val & 0x7F | 0x80));
			val >>>= 7;
			if ((val & ~0x7FL) == 0) {
				buffer.put((byte) val);
				return;
			}
		}
		buffer.put((byte) ((int) val & 0x7F | 0x80));
		val >>>= 7;
		buffer.put((byte) val);
	}

	public static/* final */void writeLong(final ByteBuffer dest, final Long val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			writeLong(dest, val);
		}
	}

	public static int writeNullFlags(final ByteBuffer dest, final Object... vals) {
		int flag = 0;

		final int length = vals.length;
		for (int i = 0; i < length; i++) {
			flag |= vals[i] == null ? 0 : 1 << i;
		}

		writeInt(dest, flag);

		return flag;
	}

	public static void writeNumber(final ByteBuffer dest, final Number val) {
		// write a tag to allow further reading
		if (val instanceof Byte) {
			dest.put((byte) 1);
			writeByte(dest, (Byte) val);
		} else if (val instanceof Short) {
			dest.put((byte) 2);
			writeShort(dest, (Short) val);
		} else if (val instanceof Integer) {
			dest.put((byte) 4);
			writeInt(dest, (Integer) val);
		} else if (val instanceof Float) {
			dest.put((byte) 8);
			dest.putFloat((Float) val);
		} else if (val instanceof Long) {
			dest.put((byte) 16);
			writeLong(dest, (Long) val);
		} else if (val instanceof Double) {
			dest.put((byte) 32);
			dest.putDouble((Double) val);
		}
	}

	public static void writeNumber(final ByteBuffer dest, final Number val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			writeNumber(dest, val);
		}
	}

	public static void writeShort(final ByteBuffer dest, final short val) {
		if (val >= 0 && val <= 254) {
			dest.put((byte) val);
			return;
		}
		dest.put(SP);
		dest.putShort(val);
	}

	public static/* final */void writeShort(final ByteBuffer dest, final Short val, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			writeShort(dest, val);
		}
	}

	public static void writeString(final ByteBuffer dest, final String value) {
		final int len = value.length();
		writeInt(dest, len);
		int c;
		for (int i = 0; i < len; i++) {
			c = value.charAt(i);
			if (c <= 0x007F) {
				dest.put((byte) c);
			} else if (c > 0x07FF) {
				dest.put((byte) (0xE0 | c >> 12 & 0x0F));
				dest.put((byte) (0x80 | c >> 6 & 0x3F));
				dest.put((byte) (0x80 | c >> 0 & 0x3F));
			} else {
				dest.put((byte) (0xC0 | c >> 6 & 0x1F));
				dest.put((byte) (0x80 | c >> 0 & 0x3F));
			}
		}
	}

	public static/* final */void writeString(final ByteBuffer dest, final String value, final int mask, final int idx) {
		if ((mask & 1 << idx) != 0) {
			writeString(dest, value);
		}
	}
}
